package com.equalexperts.logging; import org.hamcrest.CoreMatchers; import org.mutabilitydetector.AnalysisResult; import org.mutabilitydetector.Configurations; import org.mutabilitydetector.IsImmutable; import org.mutabilitydetector.locations.Dotted; import java.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import static org.hamcrest.CoreMatchers.anyOf; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; import static org.mutabilitydetector.unittesting.MutabilityMatchers.areEffectivelyImmutable; import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; /** * An OpsLogger implementation that validates arguments, but doesn't actually * log anything. Very useful for unit tests. */ public class OpsLoggerTestDouble <T extends Enum<T> & LogMessage> implements OpsLogger<T> { private final Function<OpsLogger<T>, OpsLogger<T>> nestedLoggerDecorator; private final Map<Map<String, String>, OpsLogger<T>> nestedLoggers = new ConcurrentHashMap<>(); public OpsLoggerTestDouble() { this(Function.identity()); } OpsLoggerTestDouble(Function<OpsLogger<T>, OpsLogger<T>> nestedLoggerDecorator) { this.nestedLoggerDecorator = nestedLoggerDecorator; } public static <T extends Enum<T> & LogMessage> OpsLogger<T> withSpyFunction(Function<OpsLogger<T>, OpsLogger<T>> spyFunction) { return spyFunction.apply(new OpsLoggerTestDouble<>(spyFunction)); } @Override public void log(T message, Object... details) { validate(message); ensureImmutableDetails(details); checkForTooManyFormatStringArguments(message.getMessagePattern(), details); validateFormatString(message.getMessagePattern(), details); } @Override public void logThrowable(T message, Throwable cause, Object... details) { validate(message); ensureImmutableDetails(details); assertNotNull("Throwable instance must be provided", cause); checkForTooManyFormatStringArguments(message.getMessagePattern(), details); validateFormatString(message.getMessagePattern(), details); } @Override public void close() throws IOException { throw new IllegalStateException("OpsLogger instances should not be closed by application code."); } @Override public OpsLogger<T> with(DiagnosticContextSupplier contextSupplier) { return nestedLoggers.computeIfAbsent(contextSupplier.getMessageContext(), k -> createNestedLogger()); } Function<OpsLogger<T>, OpsLogger<T>> getNestedLoggerDecorator() { return nestedLoggerDecorator; } private OpsLogger<T> createNestedLogger() { return nestedLoggerDecorator.apply(new OpsLoggerTestDouble<>(nestedLoggerDecorator)); } private void validateFormatString(String pattern, Object... details) { //noinspection ResultOfMethodCallIgnored String.format(pattern, details); } private void checkForTooManyFormatStringArguments(String pattern, Object... details) { if (details.length > 1) { /* Check for too many arguments by removing one, and expecting "not enough arguments" to happen. */ try { //noinspection ResultOfMethodCallIgnored String.format(pattern, Arrays.copyOfRange(details, 0, details.length - 1)); throw new IllegalArgumentException("Too many format string arguments provided"); } catch (MissingFormatArgumentException expected) { //expected } } } private void validate(T message) { assertNotNull("LogMessage must be provided", message); assertNotNull("MessageCode must be provided", message.getMessageCode()); assertThat("MessageCode must be provided", message.getMessageCode(), CoreMatchers.not("")); assertNotNull("MessagePattern must be provided", message.getMessagePattern()); assertThat("MessagePattern must be provided", message.getMessagePattern(), CoreMatchers.not("")); } private void ensureImmutableDetails(Object... details) { for (Object o : details) { Class<?> aClass = o.getClass(); if (!IMMUTABLE_CLASSES_FROM_THE_JDK.contains(aClass)) { assertInstancesOf(aClass, anyOf(areEffectivelyImmutable(), areImmutable())); } } } /** * The immutability detector maintains this list, but itself only uses it on fields. * We need to pass these classes themselves. */ private static final Set<Class> IMMUTABLE_CLASSES_FROM_THE_JDK; static { try { Set<Class> temp = new HashSet<>(); for (Map.Entry<Dotted, AnalysisResult> entry : Configurations.JDK_CONFIGURATION.hardcodedResults().entrySet()) { if (entry.getValue().isImmutable == IsImmutable.IMMUTABLE) { temp.add(Class.forName(entry.getKey().toString())); } } IMMUTABLE_CLASSES_FROM_THE_JDK = Collections.unmodifiableSet(temp); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } }